﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;

namespace Gems
{
    [Serializable]
    public class SuspectMatrix
    {
        readonly Int32[] _gossipList;
        readonly bool[][] _suspicionMatrix;
        readonly bool[] _failedNodes;
        private readonly Int32[] _periodSinceUpdate;

        readonly int _suspicionTimeout;
        readonly int _myRowNumber;
        readonly int _updateTimeout;

        readonly object _syncRoot = new object();

        readonly int _size;
        
        public int Size
        {
            get { return _size; }
        }

        [field:NonSerialized]
        public event Action<int> Agreed;

        public void OnAgreed(int rowNumber)
        {
            Action<int> handler = Agreed;
            if (handler != null) handler(rowNumber);
        }

        public SuspectMatrix(int size, int rowNumber, int suspicionTimeout, int updateTimeout)
        {
            _suspicionMatrix = new bool[size][];
            for (int i = 0; i < size; i++)
            {
                _suspicionMatrix[i] = new bool[size];
            }

            _failedNodes = new bool[size];
            _gossipList = new int[size];
            _periodSinceUpdate = new int[size];


            _suspicionTimeout = suspicionTimeout;
            _updateTimeout = updateTimeout;
            _myRowNumber = rowNumber;

            _size = size;
        }

        public void Tick()
        {
            lock (_syncRoot)
            {
                for (var i = 0; i < _size; i++)
                {
                    if (i == _myRowNumber) continue;

                    _gossipList[i]++;

                    if (NodeIsLive(i) && _gossipList[i] >= _suspicionTimeout)
                    {
                        _suspicionMatrix[_myRowNumber][i] = true;
                        _periodSinceUpdate[i]++;
                        Suspect(i);
                    }
                }
            }
        }

        private bool NodeIsLive(int i)
        {
            return !_failedNodes[i];
        }

        public void Merge(SuspectMatrix gossip)
        {
            Contract.Requires(gossip.Size == Size);

            lock (_syncRoot)
            {
                for (int i = 0; i < _size; i++)
                {
                    if (_gossipList[i] > gossip._gossipList[i])
                    {
                        _gossipList[i] = gossip._gossipList[i];
                        if (_gossipList[i] < _suspicionTimeout)
                            _suspicionMatrix[_myRowNumber][i] = false;

                        for (int j = 0; j < Size; j++)
                        {
                            if (_suspicionMatrix[i][j] != gossip._suspicionMatrix[i][j]) _periodSinceUpdate[j] = 0;
                            _suspicionMatrix[i][j] = gossip._suspicionMatrix[i][j];
                        }
                    }
                }

                for (int i = 0; i < _size; i++)
                {
                    if (NodeIsLive(i) && _gossipList[i] >= _suspicionTimeout)
                        Suspect(i);
                }
            }
        }

        private void Suspect(int rowNumber)
        {
            Traces.Gems.TraceEvent(TraceEventType.Verbose, Traces.ParticipantSuspected, "Participant #{0} is suspected", rowNumber);
            
            if (_periodSinceUpdate[rowNumber] > _updateTimeout || MajoritySuspected(rowNumber))
            {
                Traces.Gems.TraceEvent(TraceEventType.Information, Traces.AgreementAchieved, "Agreed that #{0} has failed.", rowNumber);
                _failedNodes[rowNumber] = true;
                OnAgreed(rowNumber);
            }
        }

        private bool MajoritySuspected(int columnNumber)
        {
            var suspecting=0;
            var failed = 0;

            for (int row = 0; row < _size; row++)
            {
                if (_failedNodes[row]) failed++;
                else
                {
                    if (_suspicionMatrix[row][columnNumber]) suspecting++;
                }
            }

            return (suspecting > (_size - failed)/2);
        }


        public bool Equals(SuspectMatrix other)
        {
            if (ReferenceEquals(null, other)) return false;
            if (ReferenceEquals(this, other)) return true;
            return _suspicionMatrix.SelectMany(m=>m).SequenceEqual(other._suspicionMatrix.SelectMany(m=>m)) && _gossipList.SequenceEqual(other._gossipList) && other._suspicionTimeout == _suspicionTimeout && other._myRowNumber == _myRowNumber && other._size == _size;
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != typeof (SuspectMatrix)) return false;
            return Equals((SuspectMatrix) obj);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                int result = (_suspicionMatrix != null ? _suspicionMatrix.GetHashCode() : 0);
                result = (result*397) ^ (_gossipList != null ? _gossipList.GetHashCode() : 0);
                result = (result*397) ^ _suspicionTimeout;
                result = (result*397) ^ _myRowNumber;
                result = (result*397) ^ _size;
                return result;
            }
        }

        
    }
}
